﻿// Copyright 2011 B Timmin <briant@sdf-eu.org>
// Copyright 2010 Maxime Serrano <lastchronicler@gmail.com>
// Copyright 2009, 2010 Matvei Stefarov <me@matvei.org>
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.IO;
using System.IO.Compression;
using System.Collections.Generic;
using System.Text;
using ForgeCraft.Network;

namespace ForgeCraft {
    public sealed class Session {
        public Player player;
        public DateTime loginTime;
        public bool canReceive,
                    canSend,
                    canQueue,
                    canDispose;

        Thread ioThread;
        TcpClient client;
        BinaryReader reader;
        PacketWriter writer;
        public ConcurrentQueue<Packet> outputQueue, priorityOutputQueue;

        private byte[] _Buffer;

        internal int fullPositionUpdateCounter;
        internal const int fullPositionUpdateInterval = 20;
        internal bool hasRegistered = false;

        public Session(TcpClient _client) {
            loginTime = DateTime.Now;

            _Buffer = new byte[0];

            canReceive = true;
            canQueue = true;

            outputQueue = new ConcurrentQueue<Packet>();
            priorityOutputQueue = new ConcurrentQueue<Packet>();

            client = _client;
            client.SendTimeout = 10000;
            client.ReceiveTimeout = 10000;

            NetworkStream stream = client.GetStream();

            reader = new BinaryReader(client.GetStream());
            writer = new PacketWriter(client.GetStream());

            Logger.Log("Server.CheckConnections: Incoming connection from " + GetIP().ToString(), LogType.Debug);

            ioThread = new Thread(IoLoop);
            ioThread.IsBackground = true;
            ioThread.Start();
        }

        void IoLoop() {
            Packet packet = new Packet();

            int pollInterval = 100;
            int pollCounter = 0;
            int packetsSent = 0;

            try {
                //TODO: Get rid of this sequence and handle it all through the case stmt
                LoginSequence();
               
                canSend = true;

                if (player == null) return;
                player.chunkLoaderThread = new Thread(new ThreadStart(player.chunkThread));
                player.chunkLoaderThread.IsBackground = true;
                player.chunkLoaderThread.Start();

                while (!canDispose) {
                    Thread.Sleep(30);

                    packetsSent = 0;

                    // Detect player disconnect
                    if (pollCounter > pollInterval) {
                        if (!client.Connected ||
                            (client.Client.Poll(1000, SelectMode.SelectRead) && client.Client.Available == 0)) {
                            if (player != null) {
                                Logger.Log("Session.IoLoop: Lost connection to player {0}.", LogType.Debug, player.GetLogName());
                            }
                            return;
                        }
                        pollCounter = 0;
                    }
                    pollCounter++;
                    
                    // Send priority output to player
                    while (canSend && client.Connected && packetsSent < Server.maxSessionPacketsPerTick) 
                    {
                        if (!priorityOutputQueue.Dequeue(ref packet)) break;                       
                        writer.Write(packet.data);
                        //Logger.Log("PO: " + packet.data[0], LogType.Debug);

                        // Testing code - Can be removed safely
                        /*if (packet.data[0] == 103)
                        {
                            string temp = "";
                            for (int i = 0; i < packet.data.Length; i++)
                            {
                                temp += packet.data[i].ToString() + ":";
                            }
                            Logger.Log("LL: " + temp, LogType.Debug);
                        }*/
                        packetsSent++;
                        if (packet.data[0] == (byte)PacketType.Disconnect && player != null)
                        {
                            Logger.Log("Session.IoLoop: Kick packet delivered to {0}.", LogType.Debug, player.GetLogName());
                            return;
                        }
                    }

                    // Send output to player
                    while (canSend && client.Connected && packetsSent < Server.maxSessionPacketsPerTick)
                    {
                        if (!outputQueue.Dequeue(ref packet)) break;
                        writer.Write(packet.data);
                        //Logger.Log("O: " + packet.data[0], LogType.Debug);
                        packetsSent++;
                        if (packet.data[0] == (byte)PacketType.Disconnect && player != null)
                        {
                            writer.Flush();
                            Logger.Log("Session.IoLoop: Kick packet delivered to {0}.", LogType.Debug, player.GetLogName());
                            return;
                        }
                    }

                    // Get input from player
                    while (canReceive && client.Connected && client.GetStream().DataAvailable) {
                        IncomingData();
                    }

                    if (player != null) player.Update();
                }
            } catch (ThreadAbortException ex) {
                Logger.Log("Session.IoLoop: Thread aborted: {0}", LogType.Error, ex);
            } catch (IOException ex) {
                Logger.Log("Session.IoLoop: {0}", LogType.Debug, ex.Message);
                try { Server.UnregisterPlayer(this.player); }
                catch { Logger.Log("Session.IoLoop: Failed attempting to unregister player.", LogType.Debug); }
            } catch (SocketException ex) {
                Logger.Log("Session.IoLoop: {0}", LogType.Debug, ex.Message);
#if DEBUG
                // CRASH on errors
#else
            } catch( Exception ex ) {
                Logger.Log( "Session.IoLoop: {0}", LogType.Error, ex );
#endif
            } finally {
                canQueue = false;
                canSend = false;
                canReceive = false;
                canDispose = true;
            }
        }

        private void IncomingData()
        {
            NetworkStream stream = client.GetStream();
            Builder<byte> buffer = new Builder<byte>();
            buffer.Append(_Buffer);

            while (stream.DataAvailable)
            {
                buffer.Append((byte)stream.ReadByte());
            }

            _Buffer = buffer.ToArray();
            buffer = null;

            while (_Buffer.Length > 1)
            {
                Pair<int, object[]> pair = CheckCompletePacket();
                int length = pair.First;
                if (length > 0)
                {
                    byte[] newBuffer = new byte[_Buffer.Length - length];
                    Array.Copy(_Buffer, length, newBuffer, 0, _Buffer.Length - length);
                    _Buffer = newBuffer;

                    ProcessPacket(pair.Second);
                }
                else
                {
                    break;
                }
            }
        }

        #region PacketChecking
        private Pair<int, object[]> CheckCompletePacket()
        {
            Pair<int, object[]> nPair = new Pair<int, object[]>(0, null);

            PacketType type = (PacketType)_Buffer[0];
            if (_Buffer[0] >= PacketStructure.Data.Length && _Buffer[0] != 0xFF)
            {
                Logger.Log("Got invalid packet: " + _Buffer[0], LogType.Warning);
                return nPair;
            }
            if (type != PacketType.Player && type != PacketType.PlayerLook && type != PacketType.PlayerPositionLook && type != PacketType.PlayerPosition)
            Logger.Log("Checking packet: " + type, LogType.Debug);

            string structure = (type == PacketType.Disconnect ? "bt" : PacketStructure.Data[_Buffer[0]]);
            int bufPos = 0;
            Builder<object> data = new Builder<object>();
            byte[] bytes = new byte[8];

            for (int i = 0; i < structure.Length; ++i)
            {
                switch (structure[i])
                {
                    case 'b':		// sbyte(1)
                        if ((bufPos + 1) > _Buffer.Length) return nPair;
                        if (i == 0)
                            data.Append((byte)_Buffer[bufPos]);
                        else
                            data.Append((sbyte)_Buffer[bufPos]);
                        bufPos += 1;
                        break;

                    case 's':		// short(2)
                        if ((bufPos + 2) > _Buffer.Length) return nPair;
                        data.Append((short)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(_Buffer, bufPos)));
                        bufPos += 2;
                        break;

                    case 'f':		// float(4)
                        if ((bufPos + 4) > _Buffer.Length) return nPair;
                        for (int j = 0; j < 4; ++j)
                        {
                            bytes[j] = _Buffer[bufPos + 3 - j];
                        }
                        data.Append((float)BitConverter.ToSingle(bytes, 0));
                        bufPos += 4;
                        break;
                    case 'i':		// int(4)
                        if ((bufPos + 4) > _Buffer.Length) return nPair;
                        data.Append((int)IPAddress.NetworkToHostOrder(BitConverter.ToInt32(_Buffer, bufPos)));
                        bufPos += 4;
                        break;

                    case 'd':		// double(8)
                        if ((bufPos + 8) > _Buffer.Length) return nPair;
                        for (int j = 0; j < 8; ++j)
                        {
                            bytes[j] = _Buffer[bufPos + 7 - j];
                        }
                        data.Append((double)BitConverter.ToDouble(bytes, 0));
                        bufPos += 8;
                        break;
                    case 'l':		// long(8)
                        if ((bufPos + 8) > _Buffer.Length) return nPair;
                        data.Append((long)IPAddress.NetworkToHostOrder(BitConverter.ToInt64(_Buffer, bufPos)));
                        bufPos += 8;
                        break;

                    case 't':		// string
                        if ((bufPos + 2) > _Buffer.Length) return nPair;
                        short len = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(_Buffer, bufPos));
                        if ((bufPos + 2 + len) > _Buffer.Length) return nPair;
                        data.Append((string)Encoding.UTF8.GetString(_Buffer, bufPos + 2, len));
                        bufPos += (2 + len);
                        break;

                    case 'I':		// inventory entity
                        // short; if nonnegative, byte then short.
                        InventoryItem item;
                        if ((bufPos + 2) > _Buffer.Length) return nPair;
                        item.Type = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(_Buffer, bufPos));
                        if (item.Type >= 0)
                        {
                            if ((bufPos + 3) > _Buffer.Length) return nPair;
                            item.Count = _Buffer[bufPos + 2];
                            item.Damage = (short)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(_Buffer, bufPos + 3));
                            bufPos += 5;
                        }
                        else
                        {
                            item.Count = 0;
                            item.Damage = 0;
                            bufPos += 2;
                        }
                        data.Append(item);
                        break;

                    case 'm':
                        // TODO: Add a metadata processor
                         /*while (x = read byte from stream) does not equal 127:
                             select based on value of (x >> 5):
                                 case 0: read byte from stream
                                 case 1: read short from stream
                                 case 2: read int from stream
                                 case 3: read float from stream
                                 case 4: read string from stream
                                 case 5: read short, byte, short from stream, discard it and continue to case 6
                             end select
                         end while*/


                    default:
                        throw new NotImplementedException("Unimplemented data type (recieve)");
                }
            }

            return new Pair<int, object[]>(bufPos, data.ToArray());
        }
        #endregion

        void LoginSequence() {
            // Handshake
            byte opcode = reader.ReadByte();
            //Logger.Log("In: " + opcode, LogType.Debug);
            if (opcode != (byte)PacketType.Handshake)
            {
                Logger.Log("Session.LoginSequence: Unexpected opcode in the first packet: {0}", LogType.Error, opcode);
                KickNow("Unexpected handshake message - possible protocol mismatch!");
                return;
            }

            PacketHandlers.HandlePacket(reader, this, player, opcode, 0);

            // Login
            opcode = reader.ReadByte();
            //Logger.Log("In: " + opcode, LogType.Debug);
            if (opcode != (byte)PacketType.LoginDetails) 
            {
                Logger.Log("Session.LoginSequence: Unexpected opcode in the second packet: {0}", LogType.Error, opcode);
                KickNow("Unexpected login message - possible protocol mismatch!");
                return;
            }

            PacketHandlers.HandlePacket(reader, this, player, opcode, (byte)PacketType.LoginDetails);
        }

        #region PacketProcessing

        private void ProcessPacket(object[] packet)
        {
            PacketType type = (PacketType)(byte)packet[0];

            if (type == PacketType.Player) return;

            if (type != PacketType.EntityLookAndMove)
            //Logger.Log("[Packet] " + player.name + " sent " + type, LogType.Debug);

            switch (type)
            {
                //TODO: Add login cases here.
                case PacketType.Message:
                    {
                        player.ResetIdleTimer();
                        if (Player.CheckForIllegalChars((string)packet[1]))
                        {
                            Logger.Log("Player.ParseMessage: {0} attempted to write illegal characters in chat and was kicked.",
                                        LogType.SuspiciousActivity,
                                        player.GetLogName());
                            KickNow("Illegal characters in chat.");
                            return;
                        }
                        else
                            player.ParseMessage((string)packet[1], false);
                        break;
                    }

                case PacketType.Respawn:
                    {
                        //TODO: Move this to player.cs

                        player.Despawn();
                        Transmit(PacketType.Respawn);
                        player.HasChunks = false;
                        player.info.health = 20;

                        Send(PacketWriter.MakeSpawnPosition(player.info.homePos));
                        Send(PacketWriter.MakePositionAndLook(player.info.homePos));
                        Send(PacketWriter.MakeUpdateHealth(player));
                        break;
                    }

                case PacketType.Player:
                    {
                        // Ignore.
                        break;
                    }
                case PacketType.PlayerPosition:
                    {
                        if (player.isRiding != null || player.isFrozen) return;

                        Position newPos = new Position();

                        // Read in new position
                        newPos.x = (double)packet[1];
                        newPos.y = (double)packet[2];
                        newPos.stance = (double)packet[3];
                        newPos.z = (double)packet[4];

                        // Calculate delta
                        Position delta = new Position();
                        delta.x = newPos.x - player.pos.x;
                        delta.y = newPos.y - player.pos.y;
                        delta.z = newPos.z - player.pos.z;

                        // Update player position
                        player.pos.x = newPos.x;
                        player.pos.y = newPos.y;
                        player.pos.z = newPos.z;
                        player.pos.stance = newPos.stance;

                        // Check if player should relative move or teleport
                        if (Math.Abs(delta.x) <= 4 &&
                            Math.Abs(delta.y) <= 4 &&
                            Math.Abs(delta.z) <= 4)
                        {
                            if (fullPositionUpdateCounter >= Session.fullPositionUpdateInterval)
                            {
                                player.currentWorld.SendToOthersInRange(PacketWriter.MakeEntityTeleport(player), player);
                                fullPositionUpdateCounter = 0;

                                //Logger.Log("Full Position Update!", LogType.Testing);
                            }
                            else
                            {
                                player.currentWorld.SendToOthersInRange(PacketWriter.MakeEntityRelativeMove(player, delta), player);
                                fullPositionUpdateCounter++;

                                //Logger.Log("Position Update!", LogType.Testing);
                            }
                        }
                        else
                        {
                            player.currentWorld.SendToOthersInRange(PacketWriter.MakeEntityTeleport(player), player);
                            fullPositionUpdateCounter = 0;
                            //Logger.Log("Teleport1!", LogType.Testing);
                        }

                        // Check of chunks should be loaded / unloaded
                        //player.UpdateChunks();                 
                        break;
                    }
                case PacketType.PlayerLook:
                    {
                        // TODO: Figure out this and PlayerPositionLook (see issue #1)
                        // TODO: Rotation seems to be an ever increasing/decresing byte in packets...
                        Position newPos = new Position();

                        // Read in new position and orientation
                        newPos = new Position();
                        float yaw = (float)packet[1], pitch = (float)packet[2];

                        newPos.rotation = (sbyte)(yaw * 256 / 360);
                        newPos.pitch = (sbyte)(pitch * 256 / 360);

                        // Update player's position and orientation
                        player.pos.stance = newPos.stance;
                        player.pos.rotation = newPos.rotation;
                        player.pos.pitch = newPos.pitch;

                        Logger.Log("Position: " + newPos.rotation, LogType.Testing);

                        // Only teleporting, because this packet seems to be sent on respawn
                        player.currentWorld.SendToOthersInRange(PacketWriter.MakeEntityTeleport(player), player);
                        //Logger.Log("Teleport2!", LogType.Testing);
                        break;
                    }

                case PacketType.PlayerPositionLook:
                    {
                        Position newPos = new Position();

                        // Read in new position and orientation
                        newPos = new Position();
                        newPos.x = (double)packet[1];
                        newPos.y = (double)packet[2];
                        newPos.stance = (double)packet[3];
                        newPos.z = (double)packet[4];
                        float yaw = (float)packet[5], pitch = (float)packet[6];

                        newPos.rotation = (sbyte)(yaw * 256 / 360);
                        newPos.pitch = (sbyte)(pitch * 256 / 360);

                        if (newPos.y == -999 && newPos.stance == -999 && player.isRiding != null) // Player in object
                        {
                            player.pos.x = player.isRiding.pos.x;
                            player.pos.y = player.isRiding.pos.y;
                            player.pos.z = player.isRiding.pos.z;

                            player.pos.stance = player.lastpos.stance;
                            player.pos.rotation = newPos.rotation;
                            player.pos.pitch = newPos.pitch;
                        }
                        else // Update player's position and orientation
                        {
                            if (!player.isFrozen)
                            {
                                player.pos.x = newPos.x;
                                player.pos.y = newPos.y;
                                player.pos.z = newPos.z;
                            }
                            player.pos.stance = newPos.stance;
                            player.pos.rotation = newPos.rotation;
                            player.pos.pitch = newPos.pitch;
                        }

                        // TODO: Using delta for this seems to move the player MASSIVE amounts
                        // it doesn't seem to be working relative to the player.

                        player.currentWorld.SendToOthersInRange(PacketWriter.MakeEntityTeleport(player), player);
                        fullPositionUpdateCounter = 0;
                        //Logger.Log("Teleport1!", LogType.Testing);

                        break;
                    }

                case PacketType.Disconnect:
                    {
                        player.DisconnectReason = "Quitting";
                        Disconnect();
                        break;
                    }

                case PacketType.PlayerDigging:
                    {
                        sbyte status = (sbyte)packet[1];
                        int x = (int)packet[2];
                        sbyte y = (sbyte)packet[3];
                        int z = (int)packet[4];
                        sbyte face = (sbyte)packet[5];

                        if (player.waitingCoords) { player.SendBlockPosition(new BlockPosition(x, (byte)y, z)); return; }

                        if (player.InstantBreak) { player.DestroyBlock(x, (byte)y, z); return; }

                        switch (status)
                        {
                            case 0:
                                ChunkBlock existingBlock = player.currentWorld.GetBlock(x, (byte)y, z);

                                switch ((Block)existingBlock.type)
                                {

                                    case Block.Torch:
                                    case Block.Crops:
                                    case Block.Reed:
                                    case Block.RedstoneWire:
                                    case Block.RedstoneTorchOff:
                                    case Block.RedstoneTorchOn:
                                    case Block.Fire:
                                    case Block.TNT:
                                        player.DestroyBlock(x, (byte)y, z);
                                        break;
                                }
                                break;

                            // Destroy block (if broken or in Instant Break mode)
                            case 2:
                                // Permission check
                                if (!player.info.group.canDelete)
                                {
                                    if (DateTime.UtcNow.Subtract(player.lastWarned).Seconds > 10)
                                    {
                                        player.Message(Color.Red + "You are not allowed to delete blocks!");
                                        player.lastWarned = DateTime.UtcNow;
                                    }
                                    break;
                                }

                                //player.Message(Color.Red + "Block Remove: " + x + " " + y + " " + z);
                                player.DestroyBlock(x, (byte)y, z);
                                break;

                            case 4:
                                //TODO: This works, but missing entity velocity or extra packets?
                                //Doesn't work as neatly it does in vanilla, so disabled for now.
                                player.Inventory.DiscardObject(player, (short)(36 + player.SlotSelected));
                                break;
                        }
                        break;
                    }

                case PacketType.PlayerBlockPlace:
                    {
                        int x = (int)packet[1];
                        sbyte y = (sbyte)packet[2];
                        int z = (int)packet[3];
                        sbyte face = (sbyte)packet[4];
                        InventoryItem block = (InventoryItem)packet[5];

                        if (player.waitingCoords) { player.SendBlockPosition(new BlockPosition(x, (byte)y, z)); return; }

                        if (x == Math.Round(player.pos.x) && y == (byte)player.pos.y && z == Math.Round(player.pos.z)) return;
                        if (x == Math.Round(player.pos.x) && y == (byte)(player.pos.y + 1) && z == Math.Round(player.pos.z)) return;

                        if (x == -1 && y == -1 && z == -1) //Special item use state update.
                        {
                            //TODO: Blocks don't require any special handling, right??
                            if (block.Type > 255) 
                            player.Message("Expecting something for " + block.Type + "? You're going to be disappointed!");
                            break;
                        }

                        //player.Message(Color.Red + "Block Place: " + x + " " + y + " " + z);
                        player.PlaceBlock(x, (byte)y, z, (byte)face, block);
                        break;
                    }

                case PacketType.WindowClick:
                    {
                        sbyte id = (sbyte)packet[1];
                        short slot = (short)packet[2];
                        byte rclick = (byte)(sbyte)packet[3];
                        short action = (short)packet[4];
                        InventoryItem item = (InventoryItem)packet[5];

                        if (item.Type < 1) item.Clear();
                        if (player.WindowHolding.Type < 1) player.WindowHolding.Clear();

                        bool success = false;

                        //Logger.Log(id + " Holding: " + player.WindowHolding + " InvBefore: " + player.Inventory.GetInventory(), LogType.Debug);
                        Logger.Log(id + " ClickID:" + id + " S:" + slot + " A:" + action + " I:" + item + " R: " + rclick, LogType.Debug);
                        
                        if (item.Type < 0 && player.WindowHolding.Type < 0) return;

                        if (player.CurrentWindow != null)
                        {
                            success = player.CurrentWindow.Click(player, slot, rclick, item);
                            //Logger.Log("Returned1: " + id + " " + action + " " + success, LogType.Debug);
                        }
                        else
                        {
                            success = player.Inventory.Click(player, slot, rclick, item);
                            //Logger.Log("Returned2: " + id + " " + action + " " + success, LogType.Debug);
                        }

                        //Send(PacketWriter
                        //Logger.Log("Sending: " + id + " " + action + " " + success, LogType.Debug);
                        Transmit(PacketType.Transaction, id, action, (sbyte)(success ? 1 : 0));
                        //Logger.Log(id + " Holding: " + player.WindowHolding + " InvBefore: " + player.Inventory.GetInventory(), LogType.Debug);
                        break;
                    }

                case PacketType.Transaction:
                    {
                        sbyte id = (sbyte)packet[1];
                        short action = (short)packet[2];
                        sbyte success = (sbyte)packet[3];

                        Logger.Log("TRN C --> S : " + action + " Result: " + success, LogType.Debug);
                        break;
                    }
                case PacketType.EntityAction:
                    {
                        int EID = (int)packet[1];
                        sbyte action = (sbyte)packet[2];

                        switch (action)
                        {
                            case 1: //Crouch - Not working?
                                //Server.SendToAllInRange(PacketWriter.MakeEntityAction(player, 1), player);
                                break;
                            case 2: //Uncrouch - Not working?
                                //Server.SendToAllInRange(PacketWriter.MakeEntityAction(player, 2), player);
                                break;
                            case 3: //Leave Bed
                                player.currentWorld.SendToOthersInRange(PacketWriter.MakeEntityAction(player, 3), player);
                                break;
                        }

                        break;
                    }

                case PacketType.Animation:
                    {
                        int EID = (int)packet[1];
                        sbyte animate = (sbyte)packet[2];

                        //TODO: Fix Crouching

                        switch (animate)
                        {
                            case 1: //Arm Swing
                                player.currentWorld.SendToOthersInRange(PacketWriter.MakeAnimation(player, 1), player);
                                break;
                            case 2: //Damage
                                break;
                            case 3: //Leave Bed
                                player.currentWorld.SendToOthersInRange(PacketWriter.MakeEntityAction(player, 3), player);
                                break;
                            case 103: //Crouch - Not working?
                                //Server.SendToAllInRange(PacketWriter.MakeEntityAction(player, 1), player);
                                break;
                            case 104: //Uncrouch - Not working?
                                //Server.SendToAllInRange(PacketWriter.MakeEntityAction(player, 2), player);
                                break;
                            case 102: //Unknown
                                break;
                        }
                       
                        break;
                    }

                case PacketType.InteractEntity:
                    {
                        int EID = (int)packet[1];
                        int target = (int)packet[2];
                        sbyte rclick = (sbyte)packet[3];
                        InventoryItem i = player.Inventory.slots[36 + player.SlotSelected];

                        Logger.Log(player.name + " holding " + i + " hit " + target + " RC: " + rclick, LogType.Debug);

                        foreach (Entity e in player.VisibleEntities)
                        {
                            if (e != this.player)
                            {
                                if (e.id == target)
                                {
                                    if (rclick == 1) // Player is not right-clicking
                                    {
                                        if (e is Player || e is MobEntity)
                                        {
                                            player.HitEntity(e, i);
                                            return;
                                        }
                                        else if (e is PickupEntity)
                                        {
                                            player.Message("Entity is a Pickup");
                                            return;
                                        }
                                        else if (e is ObjectEntity)
                                        {
                                            player.Message("Entity is an Object");
                                            return;
                                        }
                                        else
                                        {
                                            player.Message(e.id + " has no interaction handler!");
                                            return;
                                        }
                                    }
                                    else // Ride 'em cowboy!
                                    {
                                        if (player.isRiding != null)
                                        {
                                            player.currentWorld.SendToAllInRange(PacketWriter.MakeAttachEntity(player, -1), player);
                                            player.isRiding = null;
                                        }
                                        else
                                        {
                                            if (e.isRidable)
                                            {
                                                player.currentWorld.SendToAllInRange(PacketWriter.MakeAttachEntity(player, e.id), player);
                                                player.isRiding = e;
                                            }
                                        }
                                        return;
                                    }
                                }
                            }
                        }

                        Logger.Log(player.name + " found no Entity {0} to interact with", LogType.Error, target);
                    }
                    break;

                case PacketType.UpdateSign:
                    {
                        int x = (int)packet[1];
                        short y = (short)packet[2];
                        int z = (int)packet[3];
                        string Text1 = (string)packet[4];
                        string Text2 = (string)packet[5];
                        string Text3 = (string)packet[6];
                        string Text4 = (string)packet[7];

                        SignInfo si = new SignInfo();

                        si.location = x + ":" + y + ":" + z;
                        si.createdDate = DateTime.UtcNow;
                        si.createdBy = player.name;
                        si.text1 = Text1;
                        si.text2 = Text2;
                        si.text3 = Text3;
                        si.text4 = Text4;

                        this.player.currentWorld.SignList.Add(new BlockPosition(x, (byte)y, z), si);

                        //TODO: Broadcast this to all in range too.
                        player.currentWorld.SendToPlayersInRange(PacketWriter.MakeUpdateSign(si), new Position(x, y, z));

                        Logger.Log(player.name + " placed sign (" + x + ", " + y + ", " + z + "): " + Text1 + " " + Text2 + " " + Text3 + " " + Text4, LogType.UserActivity);
                    }
                    break;

                case PacketType.CloseWindow:
                    {
                        sbyte id = (sbyte)packet[1];

                        if (player.CurrentWindow != null)
                        {
                            player.CurrentWindow.Close(player);
                            player.CurrentWindow = null;
                        }
                        else
                        {
                            player.Inventory.Close(player);
                        }

                        /*if (id == 0)
                        {
                            player.Inventory.Close(player);
                        }
                        else
                        {   
                            foreach (Window w in Server.WindowList)
                            {
                                if (w.ID == (byte)id)
                                {
                                    w.Close(player);
                                    break;
                                }
                            }
                        }*/
                        break;
                    }
                case PacketType.PlayerHolding:
                    {
                        player.SlotSelected = (int)(short)packet[1];
                        break;
                    }

                default:
                    {
                        //Send(PacketWriter.MakeMessage("Error! Unrecognised packet!"));
                        Logger.Log("[Unhandled Packet] " + player.name + " sent " + type, LogType.Debug);
                        break;
                    }
            }
        }

        #endregion

        public void Disconnect()
        {
            Transmit(PacketType.Disconnect, player.DisconnectReason);

            if (player != null) {
                Server.UnregisterPlayer(player);
                player = null;
            }

            /*if (reader != null) {
                reader.Close();
                reader = null;
            }

            if (writer != null) {
                writer.Close();
                writer = null;
            }*/

            //if (client != null)
            //{
                client.Close();
                //client = null;
            //}

            
        }

        public IPAddress GetIP() {
            return ((IPEndPoint)(client.Client.RemoteEndPoint)).Address;
        }


        public void Send(Packet packet) {
            Send(packet, true);
        }

        public void Send(Packet packet, bool isHighPriority) {
            if (canQueue) {
                if (isHighPriority) {
                    //Logger.Log("HP:" + packet.data[0].ToString(), LogType.Debug);
                    priorityOutputQueue.Enqueue(packet);
                } else {
                    //Logger.Log("S:" + packet.data[0].ToString(), LogType.Debug);
                    outputQueue.Enqueue(packet);
                }
            }
        }


        #region RawPacketSender

        //This was added in as a way to verify the packet before sending.
        //TODO: Find the overflow on spawning entitiy? (er, think thats sorted now)
        public void Transmit(PacketType type, params object[] args)
        {
            //Logger.Log("Transmitting: " + type + "(" + (byte)type + ")", LogType.Debug);
            string structure = (type == PacketType.Disconnect ? "bt" : PacketStructure.Data[(byte)type]);

            Builder<Byte> packetConstruct = new Builder<Byte>();
            packetConstruct.Append((byte)type);

            byte[] bytes;
            int current = 1;
            try
            {
                for (int i = 1; i < structure.Length; ++i)
                {
                    current = i;
                    switch (structure[i])
                    {
                        case 'b':		// sbyte(1)
                            packetConstruct.Append((byte)(sbyte)args[i - 1]);
                            break;

                        case 's':		// short(2)
                            packetConstruct.Append(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)args[i - 1])));
                            break;

                        case 'f':		// float(4)
                            bytes = BitConverter.GetBytes((float)args[i - 1]);
                            for (int j = 3; j >= 0; --j)
                            {
                                packetConstruct.Append(bytes[j]);
                            }
                            //packetConstruct.Append(bytes);
                            break;

                        case 'i':		// int(4)
                            packetConstruct.Append(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((int)args[i - 1])));
                            break;

                        case 'd':		// double(8)
                            bytes = BitConverter.GetBytes((double)args[i - 1]);
                            for (int j = 7; j >= 0; --j)
                            {
                                packetConstruct.Append(bytes[j]);
                            }
                            //packetConstruct.Append(bytes);
                            break;

                        case 'l':		// long(8)
                            packetConstruct.Append(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)args[i - 1])));
                            break;

                        case 't':		// string
                            bytes = Encoding.UTF8.GetBytes((string)args[i - 1]);
                            packetConstruct.Append(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)bytes.Length)));
                            packetConstruct.Append(bytes);
                            break;

                        case 'x':		// byte array
                            packetConstruct.Append((byte[])args[i - 1]);
                            break;

                        case 'I':		// inventory entity
                            // short; if nonnegative, byte then short.
                            InventoryItem item = (InventoryItem)args[i - 1];
                            packetConstruct.Append(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(item.Type)));
                            if (item.Type >= 0)
                            {
                                packetConstruct.Append(item.Count);
                                packetConstruct.Append(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(item.Damage)));
                            }
                            break;

                        case 'M':       //Metadata
                            //Datastream, Terminated by 0x7F. 
                            //packetConstruct.Append(0x01);
                            packetConstruct.Append(0x7F);
                            break;
                        default:
                            throw new NotImplementedException("Unimplemented data type (transmit)");
                    }
                }
            }
            catch (InvalidCastException)
            {
                Logger.Log("[Error] Transmitting " + type + ": expected '" + structure[current] +
                    "', got " + args[current - 1].GetType().ToString() + " for argument " + current + " (format: " + structure + ")", LogType.Error);
                throw;
            }

            Packet sendPacket = new Packet();
            sendPacket.data = packetConstruct.ToArray();

            Send(sendPacket, false);
        }

        #endregion

        // warning: not thread safe. should ONLY be called from IoThread
        public void SendNow(Packet packet) {
            //Logger.Log("SN:" + packet.data[0].ToString(),LogType.Debug);
            writer.Write(packet.data);
        }

        public void Kick(string message) {
            Transmit(PacketType.Disconnect, message);

            if (player != null)
            {
                Server.UnregisterPlayer(player);
                player = null;
            }
        }

        public void KickNow(string message) {
            Transmit(PacketType.Disconnect, message);

            if (player != null)
            {
                Server.UnregisterPlayer(player);
                player = null;
            }
        }

        internal void Write(Packet p) {
            writer.Write(p);
        }
    }
}

